iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 22
1
Modern Web

寫React的那些事系列 第 22

React Day22 - 串接React和Redux

  • 分享至 

  • xImage
  •  

Redux是一個獨立的架構,它不一定要使用在React上,因為它的機制可以補足React比較弱的地方,所以常常和React一起提到。Redux用來管理狀態的變化,而React主要關注在component呈現的地方。前幾天我們已經把Redux的action、reducer、store設定好,今天要介紹把Redux和React結合在一起的地方!

在binding Redux和React之前,還有一個觀念要說明,就是container component和presentational component,我們要先區分這兩類components,加上Redux會更得心應手。

Container component


我們需要使用container component來連結Redux,只有它會知道Redux的存在,它通常只是一個 容器 的概念,裡面裝其他components。它負責訂閱Redux(subscribe)並把Redux的state和actions傳給子元件,只傳入需要的資訊,像是取state tree需要用到的部分state傳入即可。Container component的子元件可以是container component或是presentational component。

Presentational component


大部份的component都會是presentational component,這類component不會知道Redux的存在,接收父層傳來的props,用props處理並顯示mockup和style,components通常也比較少有自己的state,如果有也是自己內部UI相關的state。

Recap 兩種components比較


不免俗的,讓我用一個table比較一下:

  | Presentational component | Container component
  | ------------- | -------------
和Redux有接觸 | 否 | 是
關注的部分 | 如何顯示style、mockup | 如何處理、更新data
取得資料 | 從props取得 | 訂閱Redux state
改變資料 | 從props取得callback | 發送Redux action
產生方式 | 手寫產生 | 通常由react-redux產生

再來,要串接react-redux


把Redux和React binding在一起,需要再透過另一個package react-redux,先使用npm安裝:

npm install react-redux --save

Step1: connnect()

我們可以透過connnect()來幫我們產生container component,它會使用store.subscribe()來偵聽某些state tree,並且提供props傳給子元件。它會回傳一個函數,就是container component,並且可以接收四個參數,以下先介紹常用的前兩個。

(1) mapStateToProps

當有定義這個參數給connect(),表示這個container component訂閱Redux store的更新。它是一個回傳部分state tree的function,這些state就是container component要傳給其他子元件的props,當這些state被更新,會自動subscribe並傳回更新後的state tree。

這邊以todos的範例來說明,會回傳需要訂閱的state並指定給:

const mapStateToProps = (state) => {
  return {
    todos: state.todos,
    filter: state.filter
  };
};

(2) mapDispatchToProps

它是一個回傳action creator的function,讓container component可以dispatch的actions都需要傳入,並且綁定dispatch,讓接收到props的component可以直接呼叫這些function。

這邊直接以範例說明:

const mapDispatchToProps = (dispatch) => {
  return {
    addTask: (task) => { dispatch(addTask(task)) },
    editTask: (idx, task) => { dispatch(editTask(idx, task)) },
    deleteTask: (idx) => { dispatch(deleteTask(idx)) },
    toggleTask: (idx) => { dispatch(toggleTask(idx)) }
  };
};

// 接收到props的component可以直接呼叫 this.props.addTask('some tasks');

沒有錯,每個action creator如果都要列出來,有時候會顯得有點麻煩&冗長,還記得Day19的時候我們有介紹到 bindActionCreators 這個function嗎?它是Redux提供的一個方法,可以把一個或多個action creator轉換成同樣名稱當作key的object,並且加上dispatch,在這邊我們可以使用它來直接綁定同一個檔案裡的action。

import { bindActionCreators } from 'redux';
import * as TodosActions from './actions/todos';

const mapDispatchToProps = (dispatch) => {
  return {
    todosActions: bindActionCreators(TodosActions, dispatch)
  };
}

// 接收到props的component可以直接呼叫 this.props.todosActions.addTask('some tasks');

最後,我們使用connect()把這兩個參數傳入:

import { connect } from 'react-redux';

const TodoAppContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

export default TodoAppContainer;

如果當你的component沒有使用到這些參數,可以這樣設定:

// 不傳state,也不傳actions
export default connect()(App);

// 傳入state,但不傳actions
export default connect(mapStateToProps)(App);

// 不傳state,但傳入actions
export default connect(null, mapDispatchToProps)(App);

加入connect()之後,就可以把原先我們設定的一些functions和todos變數都移除。

完整的app.js如下:

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import classNames from 'classnames';

import * as TodosActions from './actions/todos';
import TodoList from './components/TodoList';
import TodoAdd from './components/TodoAdd';
import '../css/style.css';

class App extends Component {
  render() {
    // 改由props接收actions
    const { todos, todosActions } = this.props;
    return (
      <div>
        <h1 className={classNames('title')}>React Todo List</h1>
        <TodoAdd addTask={todosActions.addTask} />
        <TodoList
          todos={todos}
          saveTask={todosActions.editTask}
          deleteTask={todosActions.deleteTask}
          completeTask={todosActions.toggleTask}
        />
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    todos: state.todos,
    filter: state.filter
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    todosActions: bindActionCreators(TodosActions, dispatch)
  };
};

const TodoAppContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

export default TodoAppContainer;

Step2: Provider component

接下來我們要把Redux store傳給container component,最直覺的方式就是上面再包一層root,把store當作props傳給我們的container component,所以react-redux提供了一個 <Provider> component,用來包住我們的container component。

完整的index.js如下:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import todoApp from './reducers';
import TodoAppContainer from './app';

let store = createStore(todoApp);

ReactDOM.render(
  <Provider store={store}>
    <TodoAppContainer />
  </Provider>,
  document.getElementById('main')
);

現在我們已經完成綁定Redux和React,原本React版本的todos,已經變成React Redux版本的囉!到目前這個步驟的檔案放在Git 上囉!接下來我想補足其他沒寫到的code。

加上filter


這邊要加上先前有提到的filter功能,透過這個步驟,可以更清楚看到如何切分container component。

Step1. 新增一個actions/filter.js:

import * as types from '../constants/ActionTypes';

// action creator
export function setFilter(filter){
  return {
    type: types.SET_FILTER,
    filter
  };
}

並且加一個constants/ActionTypes.js:

export const SET_FILTER = 'SET_FILTER';

改變之前我們reducers/filter.js設定的常數

case types.SET_FILTER:

Step2. 新增一個components/Filter.js:

import React, { Component } from 'react';

class Filter extends Component {
  render() {
    const { filter, filterActions } = this.props;
    return (
      <div>
        <button
          onClick={() => filterActions.setFilter('SHOW_ALL')}
          disabled={ filter === 'SHOW_ALL' }
        >All</button>
        <button
          onClick={() => filterActions.setFilter('SHOW_COMPLETED')}
          disabled={ filter === 'SHOW_COMPLETED' }
        >Completed</button>
        <button
          onClick={() => filterActions.setFilter('SHOW_UNCOMPLETED')}
          disabled={ filter === 'SHOW_UNCOMPLETED' }
        >Uncompleted</button>
      </div>
    );
  }
}

export default Filter;

Step3. 新增一個containers/FilterContainer.js

  • FilterContainer只需要傳入filter相關的state和actions。
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import * as FilterActions from '../actions/filter';
import Filter from '../components/Filter';

class App extends Component {
  render() {
    return (
      <div className="filter">
        <Filter {...this.props} />
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    filter: state.filter
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    filterActions: bindActionCreators(FilterActions, dispatch)
  };
};

const FilterContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

export default FilterContainer;

Step4. 加入FilterContainer到TodoAppContainer中

  • TodoAppContainer和FilterContainer都是container component,而FilterContainer可以是TodoAppContainer的子元件。
  • 因為現在有兩個container了,我把app.js改名為TodoAppContainer.js,並且統一移到containers資料夾下。
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import classNames from 'classnames';

import * as TodosActions from '../actions/todos';
import FilterContainer from '../containers/FilterContainer';
import TodoList from '../components/TodoList';
import TodoAdd from '../components/TodoAdd';
import '../../css/style.css';

class App extends Component {
  render() {
    const { filter, todos, todosActions } = this.props;

    // 這個container component包含了另一個container component(FilterContainer)
    return (
      <div>
        <h1 className={classNames('title')}>React Todo List</h1>
        <TodoAdd addTask={todosActions.addTask} />
        <FilterContainer />
        <TodoList
          todos={todos}
          filter={filter}
          saveTask={todosActions.editTask}
          deleteTask={todosActions.deleteTask}
          completeTask={todosActions.toggleTask}
        />
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    todos: state.todos,
    filter: state.filter
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    todosActions: bindActionCreators(TodosActions, dispatch)
  };
};

const TodoAppContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

export default TodoAppContainer;

Step5. 顯示todos的時候,判斷目前filter狀態

components/TodoItem.js:

_renderItems() {
  const { filter, todos, saveTask, deleteTask, completeTask } = this.props;

  let list = [];
  todos.forEach((todo, idx) => {
    // 這邊為了保留原本的idx,判斷filter狀態來顯示
    if (filter === 'SHOW_ALL' ||
      (filter === 'SHOW_COMPLETED' && todo.isCompleted) ||
      (filter === 'SHOW_UNCOMPLETED' && !todo.isCompleted)) {
      list.push(
        <TodoItem
          key={idx}
          idx={idx}
          todo={todo}
          saveTask={saveTask}
          deleteTask={deleteTask}
          completeTask={completeTask}
        />);
    }
  });
  return list;
}

這樣就完成,加上filter的功能囉!這部分最主要是展示container component可以依照功能分開,而container component又可以包含container component或是presentational component。

今天真的是很長的一篇XD,但我們已經學會Redux囉!完整的程式碼已經放在Git 上

參考


Redux - Usage with React


上一篇
React Day21 - Redux Store
下一篇
React Day23 - Middleware概念
系列文
寫React的那些事31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言